package com.xbibo.autoconfigure.dal.redis.dao.impl;

import com.xbibo.autoconfigure.dal.annotation.DalColumn;
import com.xbibo.autoconfigure.dal.annotation.DalEntity;
import com.xbibo.autoconfigure.dal.annotation.Idx;
import com.xbibo.autoconfigure.dal.exception.DalException;
import com.xbibo.autoconfigure.dal.redis.dao.BaseHashRedisDao;
import com.xbibo.autoconfigure.dal.redis.support.RedisEntityInfo;
import com.xbibo.autoconfigure.dal.redis.support.RedisEntityLoader;
import com.xbibo.autoconfigure.dal.redis.support.RedisOps;
import com.xbibo.autoconfigure.dal.redis.support.RedisProperty;
import com.xbibo.autoconfigure.dal.utils.BeanUtils2;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.text.MessageFormat;
import java.util.*;

/**
 * 类/接口注释
 *
 * @author jiangnan.zjn@alibaba-inc.com
 * @createDate 2017/9/23
 */
public abstract class AbstractBaseHashRedisDao<T, ID extends Serializable> implements BaseHashRedisDao<T, ID> {

    private static final Log logger = LogFactory.getLog(AbstractBaseHashRedisDao.class);
    /**
     * 实体版本标识
     */
    private final static String KEY_VERSION = "_ver";

    private final Class<T> cls;

    /**
     * 缓存存活时间
     */
    private final long timeToLive;

    /**
     * 前缀
     */
    private String prefix;

    /**
     * 缓存key
     */
    private String baseKey;

    /**
     * 对象信息
     */
    private RedisEntityInfo<T, ID> entityInfo;

    private RedisEntityMapper mapper;

    @Autowired
    private StringRedisTemplate template;

    public AbstractBaseHashRedisDao(){
        cls = (Class < T > ) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[ 0 ];
        if(cls == null){
            throw new DalException("参数错误");
        }

        DalEntity entity = cls.getAnnotation(DalEntity.class);
        if(entity == null){
            throw new DalException(cls.getSimpleName()+"未加上 DalEntity 注解");
        }
        timeToLive = entity.timeToLive();
        prefix = entity.prefix();
        //创建缓存key
        buildKey();
        //解析实体信息
        createEntityInfo();
        //设置实体类版本
        entityInfo.setVersion(entity.version());
        //获取mapper转换对象
        mapper = new RedisEntityMapper(cls);
    }

    @Override
    public T findById(ID id, RedisEntityLoader<T, ID> loader) {
        String key = getKeySpace(id);
        Map<Object, Object> map = template.opsForHash().entries(key);
        if (!checkEntityVersion(map)) {
            logger.debug("数据版本不一致，重新加载数据");
            map = Collections.emptyMap();
        }
        if(CollectionUtils.isEmpty(map)){
            if(loader == null){
                return null;
            }
            return sync(id, loader);
        }


        return mapper.toObject(map);
    }

    /**
     * 检查实体版本是否符合要求
     *
     * @param map 数据
     * @return true 正确版本
     */
    private boolean checkEntityVersion(Map<Object, Object> map) {
        if (CollectionUtils.isEmpty(map)) {
            return true;
        }
        int version = 0;
        //检查当前缓存版本是否符合
        Object verObj = map.get(KEY_VERSION);
        if (verObj != null) {
            version = Integer.parseInt(verObj.toString());
        }
        return version >= entityInfo.getVersion();
    }

    /**
     * 保存
     * @param t 实体
     * @return T
     */
    @Override
    public T save(T t){
        ID id = entityInfo.getId(t);
        if (id == null){
            throw new DalException("参未设置ID");
        }
        String key = getKeySpace(id);
        template.opsForHash().putAll(key, mapper.toHash(t));
        if (timeToLive > 0){
            //设置过期时间
            RedisOps.expire(template, key, timeToLive);
        }
        return t;
    }

    /**
     * 数据不存在，同步数据
     * @param id 主键
     * @param loader 重载
     * @return T
     */
    private synchronized T sync(ID id, RedisEntityLoader<T, ID> loader){
        T t = null;
        try {
            t = loader.load(id);
        } catch (Exception e) {
            logger.error(String.format("id=%s,数据重载异常！", id), e);
        }
        if (t != null){
            //重新写入redis
            save(t);
        }
        return t;
    }

    /**
     * 获取存储的key
     * @param id 主键
     * @return String
     */
    private String getKeySpace(ID id){
        return MessageFormat.format(baseKey,id);
    }

    private void buildKey(){
        baseKey = prefix+cls.getSimpleName()+":{0}";
    }

    /**
     * 创建对象属性，获取全部的属性
     */
    private void createEntityInfo(){
        entityInfo = new RedisEntityInfo<>(cls);
        List<Field> fields = FieldUtils.getAllFieldsList(cls);
        List<RedisProperty> properties = new ArrayList<>(fields.size());
        RedisProperty property;
        for (Field field:fields){
            property = new RedisProperty();
            if (KEY_VERSION.equals(field.getName())) {
                throw new DalException("属性名不能使用：" + KEY_VERSION);
            }

            property.setName(field.getName());
            property.setType(field.getType());
            DalColumn column = field.getAnnotation(DalColumn.class);
            if(column != null) {
                if (KEY_VERSION.equals(column.alias())) {
                    throw new DalException("属性别名不能使用：" + KEY_VERSION);
                }
                property.setAlias(column.alias());
            }

            properties.add(property);
            if (field.getAnnotation(Idx.class) != null){
                //主键特殊处理
                entityInfo.setIdProperty(property);
            }
        }
        entityInfo.setProperties(properties);
    }


    /**
     * 实体map对象
     */
    private class RedisEntityMapper {

        private Class<T> type;

        private RedisEntityMapper(Class<T> type) {
            this.type = type;
        }

        private T toObject(Map<Object, Object> hash){
            if(CollectionUtils.isEmpty(hash)){
                return null;
            }

            Map<String, RedisProperty> aliasMap = entityInfo.getAliasMap();
            Map<String, String> map = new HashMap<>(hash.size());
            hash.forEach((k,v)->{
                String key = k.toString();
                RedisProperty property = aliasMap.get(key);
                if(property != null){
                    key = property.getName();
                }
                map.put(key, v.toString());
            });
            return doFromHash(map);
        }

        private Map<String, String> toHash(T object) {
            Map<String, String> map = doToHash(object);
            Map<String, String> hash = new HashMap<>(map.size());
            Map<String, RedisProperty> nameMap = entityInfo.getNameMap();
            map.forEach((k, v) ->{
                RedisProperty property = nameMap.get(k);
                if(property != null && !StringUtils.isEmpty(property.getAlias())){
                    hash.put(property.getAlias(), v);
                }else {
                    hash.put(k,v);
                }
            });
            //添加版本参数，用来刷新缓存
            hash.put(KEY_VERSION, entityInfo.getVersion()+"");
            return hash;
        }


        private Map<String, String> doToHash(T object) {
            try {
                Map<String, String> map = BeanUtils2.describe(object);
                Map<String, String> result = new LinkedHashMap<String, String>();
                map.forEach((k, v) -> {
                    if (!"class".equals(k) && v != null) {
                        result.put(k, v);
                    }
                });
                return result;
            } catch (Exception ex) {
                throw new IllegalArgumentException("Cannot describe object " + object, ex);
            }
        }


        private T doFromHash(Map<String, String> hash) {
            T instance = org.springframework.beans.BeanUtils.instantiate(type);
            try {
                BeanUtils2.populate(instance, hash);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
            return instance;
        }
    }

}
